Skip to content

fix(auth): guard OAuth :strategy route — 404 unknown/disabled + stateless authenticate#3947

Merged
PierreBrisorgueil merged 3 commits into
masterfrom
fix/3900-oauth-strategy-guard
Jul 4, 2026
Merged

fix(auth): guard OAuth :strategy route — 404 unknown/disabled + stateless authenticate#3947
PierreBrisorgueil merged 3 commits into
masterfrom
fix/3900-oauth-strategy-guard

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What changed: GET /api/auth/:strategy and its /callback route no longer pass req.params.strategy straight into passport.authenticate(). A new isEnabledOAuthProvider() guard rejects any strategy name that is not both allowlisted (ALLOWED_PROVIDERS) and actually registered with passport, returning a clean 404 (OAUTH_PROVIDER_NOT_FOUND) instead of a synchronous throw. oauthCall now also passes { session: false } to passport.authenticate() (stateless JWT stack, no express-session middleware mounted).
  • Why: any unmatched path segment under /api/auth/* was reaching passport.authenticate(strategy) unguarded. An unknown name (e.g. probe traffic hitting /api/auth/me) made passport throw synchronously ("Unknown authentication strategy") → 500. A provider that's allowlisted but not registered (missing credentials) hit the same throw. An actually-registered provider defaulted to session: true and would fail immediately with "Login sessions require session support" on a stack with no session middleware. This closes all three off as clean, testable 404s / correct stateless auth.
  • Related issues: Closes 🔒 fix(auth): guard OAuth :strategy route — 404 unknown/disabled provider + stateless authenticate #3900

Scope

  • Module(s) impacted: modules/auth (controller only — no route/schema changes)
  • Cross-module impact: none — the one line touched outside modules/auth is a pre-existing modules/invitations integration test assertion that exercises the same /api/auth/google route; it now asserts the new deliberate 404 contract instead of "any non-404", no invitations module logic changed.
  • Risk level: low

Validation

  • npm run lint
  • npm test
  • Manual checks done (if applicable)

Guardrails check

  • No secrets or credentials introduced (.env*, secrets/**, keys, tokens)
  • No risky rename/move of core stack paths
  • Changes remain merge-friendly for downstream projects
  • Tests added or updated when behavior changed

Reviewer notes

  • The guard uses passport._strategy(strategy) — this is @api private in passport, but it is the only source-of-truth for "is this strategy registered" in passport@0.7.0 (no public accessor exists; only use()/unuse()). A config-based check would duplicate each provider's own credential-presence gating (strategies/local/{google,apple}.js) and risk drift. Deliberate trade-off, documented inline on isEnabledOAuthProvider().
  • { session: false } is passed to passport.authenticate in oauthCall (the default session: true was the root cause of the "Login sessions require session support" error on this stateless-JWT stack). It is intentionally not added to oauthCallback — passport's callback form (passport.authenticate(strategy, callback)) never calls req.logIn() / reads options.session, so it would be a dead option there. Both are commented inline for the next reader.
  • A 4-lens adversarial pre-push review already ran on this diff (security + correctness clean). It found 2 test-coverage gaps, both fixed before this PR was opened: oauthCallback's 404-guard branch is now covered by the new unit test file, and a pre-existing session-mint regression test (identity-trust logic) had its coverage restored past the new guard so it still reaches the code it was written to protect.

Notes for reviewers

  • Security considerations: this closes an unauthenticated-request path that could throw synchronously past intended error handling (500 on probe traffic); no new attack surface is introduced — the guard is strictly more restrictive than the previous unguarded call.
  • Mergeability considerations: none — isolated to modules/auth, no shared lib//config/ changes.
  • Follow-up tasks (optional): none.

Summary by CodeRabbit

  • Bug Fixes

    • OAuth sign-in and callback routes now reject unsupported or unregistered providers with a clear 404 error.
    • OAuth authentication is now handled more safely in stateless mode, reducing the chance of unwanted session behavior.
    • Existing login flows continue to redirect properly on authentication failures, with improved route handling for legacy paths.
  • Tests

    • Expanded coverage for OAuth provider validation and callback behavior.
    • Strengthened integration checks to prevent mock leakage and verify the correct error response.

…less authenticate

oauthCall and oauthCallback passed req.params.strategy straight into
passport.authenticate() with no validation: an unknown strategy made
passport throw synchronously ("Unknown authentication strategy") -> 500,
and a registered-but-unguarded provider defaulted to session:true, which
fails on this stateless JWT stack (no express-session mounted).

Add isEnabledOAuthProvider(), shared by both routes: allowlisted (in
ALLOWED_PROVIDERS) AND actually registered with passport
(passport._strategy(strategy)) -> otherwise reject with a clean 404
(OAUTH_PROVIDER_NOT_FOUND) before ever reaching passport.authenticate().
oauthCall now also passes { session: false } explicitly.

Update the invitations alias-shadow test to assert on the new deliberate
404 (code) instead of "not 404", and add strategy-registration stubs to
the existing oauthCallback integration tests so they keep exercising the
post-authenticate path. New unit tests cover unknown/disabled/enabled
strategies for oauthCall.
…ession test

Adversarial review of the #3900 guard diff found the oauthCallback
reject branch had zero coverage (all 6 existing tests stub _strategy
truthy) and that the pre-existing "should NOT mint a session" security
regression test was silently defeated: since google is unregistered in
test config, the new guard now 404s before the request ever reaches the
post-authenticate identity-trust logic the test protects.

- Add oauthCallback guard-reject unit tests (unknown + allowlisted-but-
  unregistered strategy), mirroring the existing oauthCall coverage.
- Stub passport._strategy + passport.authenticate in the session-mint
  regression test so it again reaches the post-auth "no user" branch
  instead of short-circuiting at the guard; assert the redirect carries
  the no-user error, never OAUTH_PROVIDER_NOT_FOUND.
- Reuse the existing 'oAuth, unsupported provider' AppError message for
  both new guards instead of a fresh string, for consistency.
- Add a describe-scoped afterEach(jest.restoreAllMocks()) safety net so
  a failing assertion mid-test can't leak a stubbed spy into later
  tests (clearMocks alone doesn't restore spy implementations).
@PierreBrisorgueil PierreBrisorgueil added the Fix A bug fix label Jul 4, 2026
@PierreBrisorgueil PierreBrisorgueil self-assigned this Jul 4, 2026
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2702f5bf-054a-4aec-9ab8-8a179618f267

📥 Commits

Reviewing files that changed from the base of the PR and between 9e2a11d and cdba634.

📒 Files selected for processing (2)
  • modules/auth/tests/auth.oauthCall.controller.unit.tests.js
  • modules/auth/tests/fixtures/auth-controller.mock-setup.js

Walkthrough

This PR adds an allowlist/registration guard for OAuth providers in the auth controller, rejecting unknown or unregistered strategies with a 404 before invoking Passport, switches oauthCall to stateless authentication, and updates unit and integration tests accordingly.

Changes

OAuth Provider Guard

Layer / File(s) Summary
Provider allowlist guard and stateless authenticate
modules/auth/controllers/auth.controller.js
Adds ALLOWED_PROVIDERS/ALLOWED_PROVIDER_KEYS and isEnabledOAuthProvider(), applies the guard in oauthCall and oauthCallback to return 404 for disallowed/unregistered providers, and switches oauthCall to passport.authenticate(strategy, { session: false }).
oauthCall/oauthCallback guard unit tests
modules/auth/tests/auth.oauthCall.controller.unit.tests.js
New unit test file mocking passport and dependencies, verifying 404 OAUTH_PROVIDER_NOT_FOUND for unknown/unregistered strategies and correct delegation for allowed providers.
Integration test updates for OAuth callback security and guard
modules/auth/tests/auth.integration.tests.js
Adds jest.restoreAllMocks() safety net, stubs/restores passport._strategy across multiple callback tests, and strengthens the client-asserted identity regression test assertions.
Invitations alias routing test tightened
modules/invitations/tests/invitations.integration.tests.js
Tightens the /api/auth/google alias test to assert a specific 404 with OAUTH_PROVIDER_NOT_FOUND code.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthController
  participant Passport

  Client->>AuthController: GET /api/auth/:strategy
  AuthController->>AuthController: isEnabledOAuthProvider(strategy)
  alt provider unknown or not registered
    AuthController-->>Client: 404 OAUTH_PROVIDER_NOT_FOUND
  else provider allowed
    AuthController->>Passport: authenticate(strategy, {session:false})
    Passport-->>Client: redirect to provider / callback
  end
Loading

Possibly related PRs

  • pierreb-devkit/Node#3268: Both PRs add early allowlist-style guards in auth.controller.js's oauthCallback authorization logic.
  • pierreb-devkit/Node#3496: Both PRs modify oauthCallback in the same controller file, one adding the provider allowlist gate, the other handling req.body safely.
  • pierreb-devkit/Node#3866: Both PRs strengthen oauthCallback's pre-authentication guard logic in the same file.

Suggested labels: Tests, security

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main OAuth route hardening and stateless authentication change.
Description check ✅ Passed The description matches the repository template and includes summary, scope, validation, guardrails, and reviewer notes.
Linked Issues check ✅ Passed The changes satisfy #3900 by guarding oauthCall and oauthCallback, returning 404s, using stateless auth, and adding the requested tests.
Out of Scope Changes check ✅ Passed The only extra file change is a related test assertion update, which stays within the OAuth hardening scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3900-oauth-strategy-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.70%. Comparing base (e8ef982) to head (cdba634).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3947   +/-   ##
=======================================
  Coverage   92.69%   92.70%           
=======================================
  Files         169      169           
  Lines        5557     5562    +5     
  Branches     1787     1789    +2     
=======================================
+ Hits         5151     5156    +5     
  Misses        326      326           
  Partials       80       80           
Flag Coverage Δ
integration 61.03% <77.77%> (-0.01%) ⬇️
unit 74.39% <88.88%> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9558e22...cdba634. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/auth/controllers/auth.controller.js`:
- Around line 442-466: The isEnabledOAuthProvider guard currently assumes
passport._strategy exists and is callable, so a Passport upgrade or mock can
make it throw instead of failing closed. Update the auth.controller.js check
around isEnabledOAuthProvider to verify typeof passport._strategy === 'function'
before calling it, or switch to a locally tracked registry of enabled
strategies, so the /api/auth/:strategy routes still return false for
unknown/disabled providers without risking an exception.

In `@modules/auth/tests/auth.oauthCall.controller.unit.tests.js`:
- Around line 24-110: The `beforeEach` mock setup in the `oauthCall` and
`oauthCallback` test blocks is duplicated, so extract the shared
`jest.unstable_mockModule` initialization into a reusable helper such as
`setupAuthControllerMocks()` in a test-utils module. Keep the existing mock
behavior for `mockPassport`, logger, config, users, organizations, helpers, and
analytics, and have both describe blocks call the helper instead of repeating
the same setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f44fc14a-13f7-46b3-893b-256bcd382918

📥 Commits

Reviewing files that changed from the base of the PR and between 9558e22 and 9e2a11d.

📒 Files selected for processing (4)
  • modules/auth/controllers/auth.controller.js
  • modules/auth/tests/auth.integration.tests.js
  • modules/auth/tests/auth.oauthCall.controller.unit.tests.js
  • modules/invitations/tests/invitations.integration.tests.js

Comment thread modules/auth/controllers/auth.controller.js
Comment thread modules/auth/tests/auth.oauthCall.controller.unit.tests.js
… (CodeRabbit #3947)

Deduplicates ~85 lines of identical jest.unstable_mockModule setup that was
copy-pasted across the oauthCall and oauthCallback describe blocks' beforeEach
hooks into a single setupAuthControllerMocks() helper, called from both.

Placed under tests/fixtures/ (matching lib/middlewares/tests/fixtures/) so
Jest's testPathIgnorePatterns keeps it out of testMatch — a tests/helpers/
name would have made Jest try to run it as its own (empty) test suite.
@PierreBrisorgueil PierreBrisorgueil merged commit 997c51d into master Jul 4, 2026
8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/3900-oauth-strategy-guard branch July 4, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔒 fix(auth): guard OAuth :strategy route — 404 unknown/disabled provider + stateless authenticate

1 participant